package madoop.me;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.DigestInputStream;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;


public class HashStore implements FilenameFilter
{
    private final String TEMP_NAME = ".writing";
    
    MessageDigest hash;
    ByteBuffer converter = ByteBuffer.allocate(16);
    byte[] dataBuffer = new byte[4*1024*1024];
    
    String dir;
    
    
    HashStore(String media) throws NoSuchAlgorithmException
    {
        this.dir = media;
        hash = MessageDigest.getInstance("MD5");
    }
    
    
    public synchronized long save(InputStream in) throws Exception
    {
        File tempFile = new File(dir, TEMP_NAME);
        if (tempFile.exists())
            throw new Exception("Another block saving is in progress.");

        DigestOutputStream out = new DigestOutputStream(new FileOutputStream(tempFile),hash); 
        int readLen = 0;
        try {
            while ((readLen = in.read(dataBuffer)) > 0)
                out.write(dataBuffer, 0, readLen);
        } catch (IOException e) {
            tempFile.delete();
            throw new Exception("Failed to save input stream.");
        } finally {
            out.close();
        }

        converter.clear();
        converter.put(hash.digest());
        converter.rewind();
        long handle = converter.getLong() ^ converter.getLong();
        converter.clear();
        converter.putLong(handle);
        String fileName = hex(converter.array(),0,8);
        converter.clear();
        
        File targetFile = new File(fileName);
        if (targetFile.exists())
            throw new Exception("A file with the same MD5 handle already exists: "+fileName);
        tempFile.renameTo(new File(fileName));
        return handle;
    }

    
    public synchronized void load(long handle, OutputStream out) throws Exception
    {
        converter.clear();
        converter.putLong(handle);
        String fileName = hex(converter.array(),0,8);
        File file = new File(fileName);
        DigestInputStream in = new DigestInputStream(new FileInputStream(file),hash);
        try {
            int readLen = 0;
            while ((readLen = in.read(dataBuffer)) > 0)
                out.write(dataBuffer, 0, readLen);
        } catch (IOException e) {
            throw new Exception("Failed to save input stream.");
        } finally {
            in.close();
        }

        // Check content 
        converter.clear();
        converter.put(hash.digest());
        converter.rewind();
        long computedHandle = converter.getLong() ^ converter.getLong();
        if (computedHandle != handle) {
            converter.clear();
            converter.putLong(computedHandle);
            String polluted = hex(converter.array(),0,8);
            throw new Exception("Content polluted on storage media (hash mismatch). Requested: "+fileName+", Pullted: "+polluted);
        }
    }
    
    
    public synchronized void invalidate(long handle)
    {
        converter.clear();
        converter.putLong(handle);
        File file = new File(hex(converter.array(),0,8));
        if (file.exists()) file.delete();
    }
    
    public List<Long> list(List<Long> holder)
    {
        if (holder == null) holder = new ArrayList<Long>();
        File root = new File(dir);
        root.list(this);
        return null; 
    }
    
    
    protected static String hex(long handle)
    {
        StringBuilder sb = new StringBuilder(16);
        for (int shift = 60; shift >= 0; shift-=4) {
            int half = (int)(handle >>> shift) & 0x0F;
            if (half < 10) sb.append((char)('0'+half));
            else sb.append((char)('W'+half));
        }
        return sb.toString();
    }
    
    
    protected static String hex(byte[] bytes, int offset, int length)
    {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        int end = offset + length;
        for (int i = offset; i < end; i++) {
            byte b = bytes[i];
            int half = b >>> 4;         // higher 4 bits
            if (half < 10) sb.append((char)('0'+half));
            else sb.append((char)('W'+half));

            half = b & 0x0F;            // lower 4 bits
            if (half < 10) sb.append((char)('0'+half));
            else sb.append((char)('W'+half));
        }
        return sb.toString();
    }
    
    
    protected static long hash(String hex)
    {
        long handle = 0;
        for (int i = 0; i < hex.length(); i++) {
            handle <<= 4;
            char half = hex.charAt(i);
            if (half >= '0' && half <= '9') handle |= half - '0';
            else if (half >='a' && half <= 'f') handle |= half-'a'+10;
            
        }
        return handle;
    }


    @Override
    public boolean accept(File dir, String name)
    {
        if (name.length() != 16) return false;
        for (int i = 0; i < 16; i++) {
            char c = name.charAt(i);
            if (c < '0' && c > '9' && c < 'a' && c > 'f')
                return false;
        }
        return true;
    }
}
